﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using Microsoft.Research.DynamicDataDisplay.Common.Auxiliary;
using System.Collections.Generic;
using System.ComponentModel;
using Microsoft.Research.DynamicDataDisplay.Charts.Axes;
using System.Windows.Data;
using Microsoft.Research.DynamicDataDisplay.Common;
using System.Windows.Threading;

namespace Microsoft.Research.DynamicDataDisplay.Charts
{
    /// <summary>
    /// Defines a base class for axis UI representation.
    /// Contains a number of properties that can be used to adjust ticks set and their look.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    [TemplatePart(Name = "PART_AdditionalLabelsCanvas", Type = typeof(StackCanvas))]
    [TemplatePart(Name = "PART_CommonLabelsCanvas", Type = typeof(StackCanvas))]
    [TemplatePart(Name = "PART_TicksPath", Type = typeof(Path))]
    [TemplatePart(Name = "PART_ContentsGrid", Type = typeof(Grid))]
    public abstract class AxisControl<T> : AxisControlBase
    {
        private const string templateKey = "axisControlTemplate";
        private const string additionalLabelTransformKey = "additionalLabelsTransform";

        private const string PART_AdditionalLabelsCanvas = "PART_AdditionalLabelsCanvas";
        private const string PART_CommonLabelsCanvas = "PART_CommonLabelsCanvas";
        private const string PART_TicksPath = "PART_TicksPath";
        private const string PART_ContentsGrid = "PART_ContentsGrid";

        /// <summary>
        /// Initializes a new instance of the <see cref="AxisControl&lt;T&gt;"/> class.
        /// </summary>
        protected AxisControl()
        {
            HorizontalContentAlignment = HorizontalAlignment.Stretch;
            VerticalContentAlignment = VerticalAlignment.Stretch;

            Background = Brushes.Transparent;
            //ClipToBounds = true;
            Focusable = false;

            UpdateUIResources();
            UpdateSizeGetters();
        }

        internal void MakeDependent()
        {
            independent = false;
        }

        /// <summary>
        /// This conversion is performed to make horizontal one-string and two-string labels
        /// stay at one height.
        /// </summary>
        /// <param name="placement"></param>
        /// <returns></returns>
        private static AxisPlacement GetBetterPlacement(AxisPlacement placement)
        {
            switch (placement)
            {
                case AxisPlacement.Left:
                    return AxisPlacement.Left;
                case AxisPlacement.Right:
                    return AxisPlacement.Right;
                case AxisPlacement.Top:
                    return AxisPlacement.Top;
                case AxisPlacement.Bottom:
                    return AxisPlacement.Bottom;
                default:
                    throw new NotSupportedException();
            }
        }

        #region Properties

        private AxisPlacement placement = AxisPlacement.Bottom;
        /// <summary>
        /// Gets or sets the placement of axis control.
        /// Relative positioning of parts of axis depends on this value.
        /// </summary>
        /// <value>The placement.</value>
        public AxisPlacement Placement
        {
            get { return placement; }
            set
            {
                if (placement != value)
                {
                    placement = value;
                    UpdateUIResources();
                    UpdateSizeGetters();
                }
            }
        }

        private void UpdateSizeGetters()
        {
            switch (placement)
            {
                case AxisPlacement.Left:
                case AxisPlacement.Right:
                    getSize = size => size.Height;
                    getCoordinate = p => p.Y;
                    createScreenPoint1 = d => new Point(scrCoord1, d);
                    createScreenPoint2 = (d, size) => new Point(scrCoord2 * size, d);
                    break;
                case AxisPlacement.Top:
                case AxisPlacement.Bottom:
                    getSize = size => size.Width;
                    getCoordinate = p => p.X;
                    createScreenPoint1 = d => new Point(d, scrCoord1);
                    createScreenPoint2 = (d, size) => new Point(d, scrCoord2 * size);
                    break;
                default:
                    break;
            }

            switch (placement)
            {
                case AxisPlacement.Left:
                    createDataPoint = d => new Point(0, d);
                    break;
                case AxisPlacement.Right:
                    createDataPoint = d => new Point(1, d);
                    break;
                case AxisPlacement.Top:
                    createDataPoint = d => new Point(d, 1);
                    break;
                case AxisPlacement.Bottom:
                    createDataPoint = d => new Point(d, 0);
                    break;
                default:
                    break;
            }
        }

        private void UpdateUIResources()
        {
            ResourceDictionary resources = new ResourceDictionary
            {
                Source = new Uri("/DynamicDataDisplay;component/Charts/Axes/AxisControlStyle.xaml", UriKind.Relative)
            };

            AxisPlacement placement = GetBetterPlacement(this.placement);
            ControlTemplate template = (ControlTemplate)resources[templateKey + placement.ToString()];
            Verify.AssertNotNull(template);
            var content = (FrameworkElement)template.LoadContent();

            if (ticksPath != null && ticksPath.Data != null)
            {
                GeometryGroup group = (GeometryGroup)ticksPath.Data;
                foreach (var child in group.Children)
                {
                    LineGeometry geometry = (LineGeometry)child;
                    lineGeomPool.Put(geometry);
                }
                group.Children.Clear();
            }

            ticksPath = (Path)content.FindName(PART_TicksPath);
            ticksPath.SnapsToDevicePixels = true;
            Verify.AssertNotNull(ticksPath);

            // as this method can be called not only on loading of axisControl, but when its placement changes, internal panels
            // can be not empty and their contents should be released
            if (commonLabelsCanvas != null && labelProvider != null)
            {
                foreach (UIElement child in commonLabelsCanvas.Children)
                {
                    if (child != null)
                    {
                        labelProvider.ReleaseLabel(child);
                    }
                }

                labels = null;
                commonLabelsCanvas.Children.Clear();
            }

            commonLabelsCanvas = (StackCanvas)content.FindName(PART_CommonLabelsCanvas);
            Verify.AssertNotNull(commonLabelsCanvas);
            commonLabelsCanvas.Placement = placement;

            if (additionalLabelsCanvas != null && majorLabelProvider != null)
            {
                foreach (UIElement child in additionalLabelsCanvas.Children)
                {
                    if (child != null)
                    {
                        majorLabelProvider.ReleaseLabel(child);
                    }
                }
            }

            additionalLabelsCanvas = (StackCanvas)content.FindName(PART_AdditionalLabelsCanvas);
            Verify.AssertNotNull(additionalLabelsCanvas);
            additionalLabelsCanvas.Placement = placement;

            mainGrid = (Grid)content.FindName(PART_ContentsGrid);
            Verify.AssertNotNull(mainGrid);

            mainGrid.SetBinding(Control.BackgroundProperty, new Binding { Path = new PropertyPath("Background"), Source = this });
            mainGrid.SizeChanged += new SizeChangedEventHandler(mainGrid_SizeChanged);

            Content = mainGrid;

            string transformKey = additionalLabelTransformKey + placement.ToString();
            if (resources.Contains(transformKey))
            {
                additionalLabelTransform = (Transform)resources[transformKey];
            }
        }

        void mainGrid_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (placement.IsBottomOrTop() && e.WidthChanged ||
               e.HeightChanged)
            {
                // this is performed because if not, whole axisControl's size was measured wrongly.
                InvalidateMeasure();
				UpdateUI();
            }
        }

        private bool updateOnCommonChange = true;

        internal IDisposable OpenUpdateRegion(bool forceUpdate)
        {
            return new UpdateRegionHolder<T>(this, forceUpdate);
        }

        private sealed class UpdateRegionHolder<TT> : IDisposable
        {
            private Range<TT> prevRange;
            private CoordinateTransform prevTransform;
            private AxisControl<TT> owner;
            private bool forceUpdate = false;

            public UpdateRegionHolder(AxisControl<TT> owner) : this(owner, false) { }

            public UpdateRegionHolder(AxisControl<TT> owner, bool forceUpdate)
            {
                this.owner = owner;
                owner.updateOnCommonChange = false;

                prevTransform = owner.transform;
                prevRange = owner.range;
                this.forceUpdate = forceUpdate;
            }

            #region IDisposable Members

            public void Dispose()
            {
                owner.updateOnCommonChange = true;

                bool shouldUpdate = owner.range != prevRange;

                var screenRect = owner.Transform.ScreenRect;
                var prevScreenRect = prevTransform.ScreenRect;
                if (owner.placement.IsBottomOrTop())
                {
                    shouldUpdate |= prevScreenRect.Width != screenRect.Width;
                }
                else
                {
                    shouldUpdate |= prevScreenRect.Height != screenRect.Height;
                }

                shouldUpdate |= owner.transform.DataTransform != prevTransform.DataTransform;
                shouldUpdate |= forceUpdate;

                if (shouldUpdate)
                {
                    owner.UpdateUI();
                }
                owner = null;
            }

            #endregion
        }

        private Range<T> range;
        /// <summary>
        /// Gets or sets the range, which ticks are generated for.
        /// </summary>
        /// <value>The range.</value>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Range<T> Range
        {
            get { return range; }
            set
            {
                range = value;
                if (updateOnCommonChange)
                {
                    UpdateUI();
                }
            }
        }

        private bool drawMinorTicks = true;
        /// <summary>
        /// Gets or sets a value indicating whether to show minor ticks.
        /// </summary>
        /// <value><c>true</c> if show minor ticks; otherwise, <c>false</c>.</value>
        public bool DrawMinorTicks
        {
            get { return drawMinorTicks; }
            set
            {
                if (drawMinorTicks != value)
                {
                    drawMinorTicks = value;
                    UpdateUI();
                }
            }
        }

        private bool drawMajorLabels = true;
        /// <summary>
        /// Gets or sets a value indicating whether to show major labels.
        /// </summary>
        /// <value><c>true</c> if show major labels; otherwise, <c>false</c>.</value>
        public bool DrawMajorLabels
        {
            get { return drawMajorLabels; }
            set
            {
                if (drawMajorLabels != value)
                {
                    drawMajorLabels = value;
                    UpdateUI();
                }
            }
        }

        private bool drawTicks = true;
        public bool DrawTicks
        {
            get { return drawTicks; }
            set
            {
                if (drawTicks != value)
                {
                    drawTicks = value;
                    UpdateUI();
                }
            }
        }

        #region TicksProvider

        private ITicksProvider<T> ticksProvider;
        /// <summary>
        /// Gets or sets the ticks provider - generator of ticks for given range.
        /// 
        /// Should not be null.
        /// </summary>
        /// <value>The ticks provider.</value>
        public ITicksProvider<T> TicksProvider
        {
            get { return ticksProvider; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                if (ticksProvider != value)
                {
                    DetachTicksProvider();

                    ticksProvider = value;

                    AttachTicksProvider();

                    UpdateUI();
                }
            }
        }

        private void AttachTicksProvider()
        {
            if (ticksProvider != null)
            {
                ticksProvider.Changed += ticksProvider_Changed;
            }
        }

        private void ticksProvider_Changed(object sender, EventArgs e)
        {
            UpdateUI();
        }

        private void DetachTicksProvider()
        {
            if (ticksProvider != null)
            {
                ticksProvider.Changed -= ticksProvider_Changed;
            }
        }

        #endregion

        [EditorBrowsable(EditorBrowsableState.Never)]
        public override bool ShouldSerializeContent()
        {
            return false;
        }

        protected override bool ShouldSerializeProperty(DependencyProperty dp)
        {
            // do not serialize template - for XAML serialization
            if (dp == TemplateProperty) return false;

            return base.ShouldSerializeProperty(dp);
        }

        #region MajorLabelProvider

        private LabelProviderBase<T> majorLabelProvider;
        /// <summary>
        /// Gets or sets the major label provider, which creates labels for major ticks.
        /// If null, major labels will not be shown.
        /// </summary>
        /// <value>The major label provider.</value>
        public LabelProviderBase<T> MajorLabelProvider
        {
            get { return majorLabelProvider; }
            set
            {
                if (majorLabelProvider != value)
                {
                    DetachMajorLabelProvider();

                    majorLabelProvider = value;

                    AttachMajorLabelProvider();

                    UpdateUI();
                }
            }
        }

        private void AttachMajorLabelProvider()
        {
            if (majorLabelProvider != null)
            {
                majorLabelProvider.Changed += majorLabelProvider_Changed;
            }
        }

        private void majorLabelProvider_Changed(object sender, EventArgs e)
        {
            UpdateUI();
        }

        private void DetachMajorLabelProvider()
        {
            if (majorLabelProvider != null)
            {
                majorLabelProvider.Changed -= majorLabelProvider_Changed;
            }
        }

        #endregion

        #region LabelProvider

        private LabelProviderBase<T> labelProvider;
        /// <summary>
        /// Gets or sets the label provider, which generates labels for axis ticks.
        /// Should not be null.
        /// </summary>
        /// <value>The label provider.</value>
        [NotNull]
        public LabelProviderBase<T> LabelProvider
        {
            get { return labelProvider; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                if (labelProvider != value)
                {
                    DetachLabelProvider();

                    labelProvider = value;

                    AttachLabelProvider();

                    UpdateUI();
                }
            }
        }

        private void AttachLabelProvider()
        {
            if (labelProvider != null)
            {
                labelProvider.Changed += labelProvider_Changed;
            }
        }

        private void labelProvider_Changed(object sender, EventArgs e)
        {
            UpdateUI();
        }

        private void DetachLabelProvider()
        {
            if (labelProvider != null)
            {
                labelProvider.Changed -= labelProvider_Changed;
            }
        }

        #endregion

        private CoordinateTransform transform = CoordinateTransform.CreateDefault();
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public CoordinateTransform Transform
        {
            get { return transform; }
            set
            {
                transform = value;
                if (updateOnCommonChange)
                {
                    UpdateUI();
                }
            }
        }

        #endregion

        private const double defaultSmallerSize = 1;
        private const double defaultBiggerSize = 150;
        protected override Size MeasureOverride(Size constraint)
        {
			var baseSize = base.MeasureOverride(constraint);

            mainGrid.Measure(constraint);
            Size gridSize = mainGrid.DesiredSize;
            Size result = gridSize;

            bool isHorizontal = placement == AxisPlacement.Bottom || placement == AxisPlacement.Top;
            if (Double.IsInfinity(constraint.Width) && isHorizontal)
            {
                result = new Size(defaultBiggerSize, gridSize.Height != 0 ? gridSize.Height : defaultSmallerSize);
            }
            else if (Double.IsInfinity(constraint.Height) && !isHorizontal)
            {
                result = new Size(gridSize.Width != 0 ? gridSize.Width : defaultSmallerSize, defaultBiggerSize);
            }

            return result;
        }

        //protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        //{
        //    base.OnRenderSizeChanged(sizeInfo);

        //    bool isHorizontal = placement == AxisPlacement.Top || placement == AxisPlacement.Bottom;
        //    if (isHorizontal && sizeInfo.WidthChanged || !isHorizontal && sizeInfo.HeightChanged)
        //    {
        //        UpdateUIRepresentation();
        //    }
        //}

        private void InitTransform(Size newRenderSize)
        {
            Rect dataRect = CreateDataRect();

            transform = transform.WithRects(dataRect, new Rect(newRenderSize));
        }

        private Rect CreateDataRect()
        {
            double min = convertToDouble(range.Min);
            double max = convertToDouble(range.Max);

            Rect dataRect;
            switch (placement)
            {
                case AxisPlacement.Left:
                case AxisPlacement.Right:
                    dataRect = new Rect(new Point(min, min), new Point(max, max));
                    break;
                case AxisPlacement.Top:
                case AxisPlacement.Bottom:
                    dataRect = new Rect(new Point(min, min), new Point(max, max));
                    break;
                default:
                    throw new NotSupportedException();
            }
            return dataRect;
        }

        /// <summary>
        /// Gets the Path with ticks strokes.
        /// </summary>
        /// <value>The ticks path.</value>
        public Path TicksPath
        {
            get { return ticksPath; }
        }

        private Grid mainGrid;
        private StackCanvas additionalLabelsCanvas;
        private StackCanvas commonLabelsCanvas;
        private Path ticksPath;
        private bool rendered = false;
        protected override void OnRender(DrawingContext dc)
        {
            base.OnRender(dc);

            if (!rendered)
            {
                UpdateUI();
            }
            rendered = true;
        }

        private bool independent = true;

        private double scrCoord1 = 0; // px
        private double scrCoord2 = 10; // px
        /// <summary>
        /// Gets or sets the size of main axis ticks.
        /// </summary>
        /// <value>The size of the tick.</value>
        public double TickSize
        {
            get { return scrCoord2; }
            set
            {
                if (scrCoord2 != value)
                {
                    scrCoord2 = value;
                    UpdateUI();
                }
            }
        }

        private GeometryGroup geomGroup = new GeometryGroup();
        internal void UpdateUI()
        {
            if (range.IsEmpty)
                return;

            if (transform == null) return;

            if (independent)
            {
                InitTransform(RenderSize);
            }

            bool isHorizontal = Placement == AxisPlacement.Bottom || Placement == AxisPlacement.Top;
            if (transform.ScreenRect.Width == 0 && isHorizontal
                || transform.ScreenRect.Height == 0 && !isHorizontal)
                return;

            if (!IsMeasureValid)
            {
                InvalidateMeasure();
            }

            CreateTicks();

            // removing unfinite screen ticks
            var tempTicks = new List<T>(ticks);
            var tempScreenTicks = new List<double>(ticks.Length);
            var tempLabels = new List<UIElement>(labels);

            int i = 0;
            while (i < tempTicks.Count)
            {
                T tick = tempTicks[i];
                double screenTick = getCoordinate(createDataPoint(convertToDouble(tick)).DataToScreen(transform));
                if (screenTick.IsFinite())
                {
                    tempScreenTicks.Add(screenTick);
                    i++;
                }
                else
                {
                    tempTicks.RemoveAt(i);
                    tempLabels.RemoveAt(i);
                }
            }

            ticks = tempTicks.ToArray();
            screenTicks = tempScreenTicks.ToArray();
            labels = tempLabels.ToArray();

            // saving generated lines into pool
            for (i = 0; i < geomGroup.Children.Count; i++)
            {
                var geometry = (LineGeometry)geomGroup.Children[i];
                lineGeomPool.Put(geometry);
            }

            geomGroup = new GeometryGroup();
            geomGroup.Children = new GeometryCollection(lineGeomPool.Count);

            if (drawTicks)
                DoDrawTicks(screenTicks, geomGroup.Children);

            if (drawMinorTicks)
                DoDrawMinorTicks(geomGroup.Children);

            ticksPath.Data = geomGroup;

            DoDrawCommonLabels(screenTicks);

            if (drawMajorLabels)
                DoDrawMajorLabels();

            ScreenTicksChanged.Raise(this);
        }

        bool drawTicksOnEmptyLabel = false;
        /// <summary>
        /// Gets or sets a value indicating whether to draw ticks on empty label.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if draw ticks on empty label; otherwise, <c>false</c>.
        /// </value>
        public bool DrawTicksOnEmptyLabel
        {
            get { return drawTicksOnEmptyLabel; }
            set
            {
                if (drawTicksOnEmptyLabel != value)
                {
                    drawTicksOnEmptyLabel = value;
                    UpdateUI();
                }
            }
        }

        private readonly ResourcePool<LineGeometry> lineGeomPool = new ResourcePool<LineGeometry>();
        private void DoDrawTicks(double[] screenTicksX, ICollection<Geometry> lines)
        {
            for (int i = 0; i < screenTicksX.Length; i++)
            {
                if (labels[i] == null && !drawTicksOnEmptyLabel)
                    continue;

                Point p1 = createScreenPoint1(screenTicksX[i]);
                Point p2 = createScreenPoint2(screenTicksX[i], 1);

                LineGeometry line = lineGeomPool.GetOrCreate();

                line.StartPoint = p1;
                line.EndPoint = p2;
                lines.Add(line);
            }
        }

        private double GetRangesRatio(Range<T> nominator, Range<T> denominator)
        {
            double nomMin = ConvertToDouble(nominator.Min);
            double nomMax = ConvertToDouble(nominator.Max);
            double denMin = ConvertToDouble(denominator.Min);
            double denMax = ConvertToDouble(denominator.Max);

            return (nomMax - nomMin) / (denMax - denMin);
        }

        Transform additionalLabelTransform = null;
        private void DoDrawMajorLabels()
        {
            ITicksProvider<T> majorTicksProvider = ticksProvider.MajorProvider;
            additionalLabelsCanvas.Children.Clear();

            if (majorTicksProvider != null && majorLabelProvider != null)
            {
                additionalLabelsCanvas.Visibility = Visibility.Visible;

                Size renderSize = RenderSize;
                var majorTicks = majorTicksProvider.GetTicks(range, DefaultTicksProvider.DefaultTicksCount);

                double[] screenCoords = majorTicks.Ticks.Select(tick => createDataPoint(convertToDouble(tick))).
                    Select(p => p.DataToScreen(transform)).Select(p => getCoordinate(p)).ToArray();

                // todo this is not the best decision - when displaying, for example,
                // milliseconds, it causes to create hundreds and thousands of textBlocks.
                double rangesRatio = GetRangesRatio(majorTicks.Ticks.GetPairs().ToArray()[0], range);

                object info = majorTicks.Info;
                MajorLabelsInfo newInfo = new MajorLabelsInfo
                {
                    Info = info,
                    MajorLabelsCount = (int)Math.Ceiling(rangesRatio)
                };

                var newMajorTicks = new TicksInfo<T>
                {
                    Info = newInfo,
                    Ticks = majorTicks.Ticks,
                    TickSizes = majorTicks.TickSizes
                };

                UIElement[] additionalLabels = MajorLabelProvider.CreateLabels(newMajorTicks);

                for (int i = 0; i < additionalLabels.Length; i++)
                {
                    if (screenCoords[i].IsNaN())
                        continue;

                    UIElement tickLabel = additionalLabels[i];

                    tickLabel.Measure(renderSize);

                    StackCanvas.SetCoordinate(tickLabel, screenCoords[i]);
                    StackCanvas.SetEndCoordinate(tickLabel, screenCoords[i + 1]);

                    if (tickLabel is FrameworkElement)
                        ((FrameworkElement)tickLabel).LayoutTransform = additionalLabelTransform;

                    additionalLabelsCanvas.Children.Add(tickLabel);
                }
            }
            else
            {
                additionalLabelsCanvas.Visibility = Visibility.Collapsed;
            }
        }

        private int prevMinorTicksCount = DefaultTicksProvider.DefaultTicksCount;
        private const int maxTickArrangeIterations = 12;
        private void DoDrawMinorTicks(ICollection<Geometry> lines)
        {
            ITicksProvider<T> minorTicksProvider = ticksProvider.MinorProvider;
            if (minorTicksProvider != null)
            {
                int minorTicksCount = prevMinorTicksCount;
                int prevActualTicksCount = -1;
                ITicksInfo<T> minorTicks;
                TickCountChange result = TickCountChange.OK;
                TickCountChange prevResult;
                int iteration = 0;
                do
                {
                    Verify.IsTrue(++iteration < maxTickArrangeIterations);

                    minorTicks = minorTicksProvider.GetTicks(range, minorTicksCount);

                    prevActualTicksCount = minorTicks.Ticks.Length;
                    prevResult = result;
                    result = CheckMinorTicksArrangement(minorTicks);
                    if (prevResult == TickCountChange.Decrease && result == TickCountChange.Increase)
                    {
                        // stop tick number oscillating
                        result = TickCountChange.OK;
                    }
                    if (result == TickCountChange.Decrease)
                    {
                        int newMinorTicksCount = minorTicksProvider.DecreaseTickCount(minorTicksCount);
                        if (newMinorTicksCount == minorTicksCount)
                        {
                            result = TickCountChange.OK;
                        }
                        minorTicksCount = newMinorTicksCount;
                    }
                    else if (result == TickCountChange.Increase)
                    {
                        int newCount = minorTicksProvider.IncreaseTickCount(minorTicksCount);
                        if (newCount == minorTicksCount)
                        {
                            result = TickCountChange.OK;
                        }
                        minorTicksCount = newCount;
                    }

                } while (result != TickCountChange.OK);
                prevMinorTicksCount = minorTicksCount;

                double[] sizes = minorTicks.TickSizes;

                double[] screenCoords = minorTicks.Ticks.Select(
                    coord => getCoordinate(createDataPoint(convertToDouble(coord)).
                        DataToScreen(transform))).ToArray();

                minorScreenTicks = new MinorTickInfo<double>[screenCoords.Length];
                for (int i = 0; i < screenCoords.Length; i++)
                {
                    minorScreenTicks[i] = new MinorTickInfo<double>(sizes[i], screenCoords[i]);
                }

                for (int i = 0; i < screenCoords.Length; i++)
                {
                    double screenCoord = screenCoords[i];

                    Point p1 = createScreenPoint1(screenCoord);
                    Point p2 = createScreenPoint2(screenCoord, sizes[i]);

                    LineGeometry line = lineGeomPool.GetOrCreate();
                    line.StartPoint = p1;
                    line.EndPoint = p2;

                    lines.Add(line);
                }
            }
        }

        private TickCountChange CheckMinorTicksArrangement(ITicksInfo<T> minorTicks)
        {
            Size renderSize = RenderSize;
            TickCountChange result = TickCountChange.OK;
            if (minorTicks.Ticks.Length * 3 > getSize(renderSize))
                result = TickCountChange.Decrease;
            else if (minorTicks.Ticks.Length * 6 < getSize(renderSize))
                result = TickCountChange.Increase;
            return result;
        }

        private bool isStaticAxis = false;
        /// <summary>
        /// Gets or sets a value indicating whether this instance is a static axis.
        /// If axis is static, its labels from sides are shifted so that they are not clipped by axis bounds.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is static axis; otherwise, <c>false</c>.
        /// </value>
        public bool IsStaticAxis
        {
            get { return isStaticAxis; }
            set
            {
                if (isStaticAxis != value)
                {
                    isStaticAxis = value;
                    UpdateUI();
                }
            }
        }

        private double ToScreen(T value)
        {
            return getCoordinate(createDataPoint(convertToDouble(value)).DataToScreen(transform));
        }

        private double staticAxisMargin = 1; // px

        private void DoDrawCommonLabels(double[] screenTicksX)
        {
            Size renderSize = RenderSize;

            commonLabelsCanvas.Children.Clear();

#if DEBUG
            if (labels != null)
            {
                foreach (FrameworkElement item in labels)
                {
                    if (item != null)
                        Debug.Assert(item.Parent == null);
                }
            }
#endif

            double minCoordUnsorted = ToScreen(range.Min);
            double maxCoordUnsorted = ToScreen(range.Max);

            double minCoord = Math.Min(minCoordUnsorted, maxCoordUnsorted);
            double maxCoord = Math.Max(minCoordUnsorted, maxCoordUnsorted);

            double maxCoordDiff = (maxCoord - minCoord) / labels.Length / 2.0;

            double minCoordToAdd = minCoord - maxCoordDiff;
            double maxCoordToAdd = maxCoord + maxCoordDiff;

            for (int i = 0; i < ticks.Length; i++)
            {
                FrameworkElement tickLabel = (FrameworkElement)labels[i];
                if (tickLabel == null) continue;

                Debug.Assert(((FrameworkElement)tickLabel).Parent == null);

                tickLabel.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));

                double screenX = screenTicksX[i];
                double coord = screenX;

                tickLabel.HorizontalAlignment = HorizontalAlignment.Center;
                tickLabel.VerticalAlignment = VerticalAlignment.Center;

                if (isStaticAxis)
                {
                    // getting real size of label
                    tickLabel.Measure(renderSize);
                    Size tickLabelSize = tickLabel.DesiredSize;

                    if (Math.Abs(screenX - minCoord) < maxCoordDiff)
                    {
                        coord = minCoord + staticAxisMargin;
                        if (placement.IsBottomOrTop())
                            tickLabel.HorizontalAlignment = HorizontalAlignment.Left;
                        else
                            tickLabel.VerticalAlignment = VerticalAlignment.Top;
                    }
                    else if (Math.Abs(screenX - maxCoord) < maxCoordDiff)
                    {
                        coord = maxCoord - getSize(tickLabelSize) / 2 - staticAxisMargin;
                        if (!placement.IsBottomOrTop())
                        {
                            tickLabel.VerticalAlignment = VerticalAlignment.Bottom;
                            coord = maxCoord - staticAxisMargin;
                        }
                    }
                }

                // label is out of visible area
                if (coord < minCoord || coord > maxCoord)
                {
                    continue;
                }

                if (coord.IsNaN())
                    continue;

                StackCanvas.SetCoordinate(tickLabel, coord);

                commonLabelsCanvas.Children.Add(tickLabel);
            }
        }

        private double GetCoordinateFromTick(T tick)
        {
            return getCoordinate(createDataPoint(convertToDouble(tick)).DataToScreen(transform));
        }

        private Func<T, double> convertToDouble;
        /// <summary>
        /// Gets or sets the convertion of tick to double.
        /// Should not be null.
        /// </summary>
        /// <value>The convert to double.</value>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Func<T, double> ConvertToDouble
        {
            get { return convertToDouble; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value");

                convertToDouble = value;
                UpdateUI();
            }
        }

        internal event EventHandler ScreenTicksChanged;
        private double[] screenTicks;
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public double[] ScreenTicks
        {
            get { return screenTicks; }
        }

        private MinorTickInfo<double>[] minorScreenTicks;
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        public MinorTickInfo<double>[] MinorScreenTicks
        {
            get { return minorScreenTicks; }
        }

        ITicksInfo<T> ticksInfo;
        private T[] ticks;
        private UIElement[] labels;
        private const double increaseRatio = 3.0;
        private const double decreaseRatio = 1.6;

        private Func<Size, double> getSize = size => size.Width;
        private Func<Point, double> getCoordinate = p => p.X;
        private Func<double, Point> createDataPoint = d => new Point(d, 0);

        private Func<double, Point> createScreenPoint1 = d => new Point(d, 0);
        private Func<double, double, Point> createScreenPoint2 = (d, size) => new Point(d, size);

        private int previousTickCount = DefaultTicksProvider.DefaultTicksCount;
        private void CreateTicks()
        {
            TickCountChange result = TickCountChange.OK;
            TickCountChange prevResult;

            int prevActualTickCount = -1;

            int tickCount = previousTickCount;
            int iteration = 0;

            do
            {
                Verify.IsTrue(++iteration < maxTickArrangeIterations);

                ticksInfo = ticksProvider.GetTicks(range, tickCount);
                ticks = ticksInfo.Ticks;

                if (ticks.Length == prevActualTickCount)
                {
                    result = TickCountChange.OK;
                    break;
                }

                prevActualTickCount = ticks.Length;

                if (labels != null)
                {
                    for (int i = 0; i < labels.Length; i++)
                    {
                        labelProvider.ReleaseLabel(labels[i]);
                    }
                }

                labels = labelProvider.CreateLabels(ticksInfo);

                prevResult = result;
                result = CheckLabelsArrangement(labels, ticks);

                if (prevResult == TickCountChange.Decrease && result == TickCountChange.Increase)
                {
                    // stop tick number oscillating
                    result = TickCountChange.OK;
                }

                if (result != TickCountChange.OK)
                {
                    int prevTickCount = tickCount;
                    if (result == TickCountChange.Decrease)
                        tickCount = ticksProvider.DecreaseTickCount(tickCount);
                    else
                    {
                        tickCount = ticksProvider.IncreaseTickCount(tickCount);
                        //DebugVerify.Is(tickCount >= prevTickCount);
                    }

                    // ticks provider could not create less ticks or tick number didn't change
                    if (tickCount == 0 || prevTickCount == tickCount)
                    {
                        tickCount = prevTickCount;
                        result = TickCountChange.OK;
                    }
                }
            } while (result != TickCountChange.OK);

            previousTickCount = tickCount;
        }

        private TickCountChange CheckLabelsArrangement(UIElement[] labels, T[] ticks)
        {
            var actualLabels = labels.Select((label, i) => new { Label = label, Index = i })
                .Where(el => el.Label != null)
                .Select(el => new { Label = el.Label, Tick = ticks[el.Index] })
                .ToList();

            actualLabels.ForEach(item => item.Label.Measure(RenderSize));

            var sizeInfos = actualLabels.Select(item =>
                new { X = GetCoordinateFromTick(item.Tick), Size = getSize(item.Label.DesiredSize) })
                .OrderBy(item => item.X).ToArray();

            TickCountChange res = TickCountChange.OK;

            int increaseCount = 0;
            for (int i = 0; i < sizeInfos.Length - 1; i++)
            {
                if ((sizeInfos[i].X + sizeInfos[i].Size * decreaseRatio) > sizeInfos[i + 1].X)
                {
                    res = TickCountChange.Decrease;
                    break;
                }
                if ((sizeInfos[i].X + sizeInfos[i].Size * increaseRatio) < sizeInfos[i + 1].X)
                {
                    increaseCount++;
                }
            }
            if (increaseCount > sizeInfos.Length / 2)
                res = TickCountChange.Increase;

            return res;
        }
    }

    [DebuggerDisplay("{X} + {Size}")]
    internal sealed class SizeInfo : IComparable<SizeInfo>
    {
        public double Size { get; set; }
        public double X { get; set; }


        public int CompareTo(SizeInfo other)
        {
            return X.CompareTo(other.X);
        }
    }

    internal enum TickCountChange
    {
        Increase = -1,
        OK = 0,
        Decrease = 1
    }

    /// <summary>
    /// Represents an auxiliary structure for storing additional info during major DateTime labels generation.
    /// </summary>
    public struct MajorLabelsInfo
    {
        public object Info { get; set; }
        public int MajorLabelsCount { get; set; }

        public override string ToString()
        {
            return String.Format("{0}, Count={1}", Info, MajorLabelsCount);
        }
    }
}
